package net.aiion.weave.impl.bytecode.scanning;

import java.io.IOException;
import java.util.Objects;
import java.util.Stack;

import net.aiion.weave.asm.ClassWriter;
import net.aiion.weave.impl.bytecode.BytecodeClassWriterVisitor;
import net.aiion.weave.impl.bytecode.BytecodeStream;
import net.aiion.weave.impl.bytecode.BytecodeTypeImpl;
import net.aiion.weave.impl.bytecode.events.BytecodeEventImpl;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.bytecode.BytecodeContext;
import net.aiion.weave.spi.bytecode.BytecodeException;
import net.aiion.weave.spi.bytecode.BytecodeSink;
import net.aiion.weave.spi.bytecode.BytecodeType;
import net.aiion.weave.spi.bytecode.BytecodeVisitor;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;

public class BytecodeStoreSinkImpl implements BytecodeSink
{
    private final BytecodeStream bytecodeStream;
    private final ClassWriter classWriter;
    private final Stack<BytecodeVisitor> bytecodeVisitors;

    private boolean isNatural;
    
    private BytecodeType bytecodeType;

    private boolean inField;
    private boolean inMethod;

    public BytecodeStoreSinkImpl() {
        bytecodeStream = new BytecodeStream();
        classWriter = new ClassWriter(0);
        bytecodeVisitors = new Stack<>();
        bytecodeVisitors.push(new BytecodeClassWriterVisitor(classWriter));
    }

    @Override
    public void handleBytecodeEvent(final BytecodeContext aBytecodeContext) {
        final BytecodeEvent bytecodeEvent = aBytecodeContext.getBytecodeEvent();

        final BytecodeVisitor currentBytecodeVisitor = bytecodeVisitors.peek();
        final BytecodeVisitor newBytecodeVisitor = bytecodeEvent.acceptBytecodeVisitor(currentBytecodeVisitor);
        if (newBytecodeVisitor == null) {
            bytecodeVisitors.pop();
        } else if (!Objects.equals(currentBytecodeVisitor, newBytecodeVisitor)) {
            bytecodeVisitors.push(newBytecodeVisitor);
        }

        if (bytecodeEvent instanceof BytecodeEventImpl) {
            isNatural = isNatural && ((BytecodeEventImpl) bytecodeEvent).isNatural();
        }

        switch (bytecodeEvent.getType()) {
        case BytecodeEvent.CLASS_START:
            handleClassStartBytecodeEvent(aBytecodeContext, bytecodeEvent.asClassStart());
            break;
        case BytecodeEvent.ANNOTATION_START:
            handleAnnotationStartBytecodeEvent(aBytecodeContext, bytecodeEvent.asAnnotationStart());
            break;
        case BytecodeEvent.FIELD_START:
            inField = true;
            break;
        case BytecodeEvent.FIELD_END:
            inField = false;
            break;
        case BytecodeEvent.METHOD_START:
            inMethod = true;
            break;
        case BytecodeEvent.METHOD_END:
            inMethod = false;
            break;
        case BytecodeEvent.CLASS_END:
            handleClassEndBytecodeEvent(aBytecodeContext, bytecodeEvent.asClassEnd());
            break;
        }
    }

    private void handleClassStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassStartBytecodeEvent aClassStartBytecodeEvent)
    {
        isNatural = true;
        if (aClassStartBytecodeEvent instanceof BytecodeEventImpl) {
            isNatural = ((BytecodeEventImpl) aClassStartBytecodeEvent).isNatural();
        }
        
        bytecodeType = new BytecodeTypeImpl();
        bytecodeType.setTypeName(aClassStartBytecodeEvent.getName().replace('/', '.'));
        bytecodeType.setSuperTypeName(aClassStartBytecodeEvent.getSuperName().replace('/', '.'));

        addInterfaceNames(aClassStartBytecodeEvent.getInterfaceNames());
    }
    
    private void addInterfaceNames(final String[] aInterfaceNames) {
    	if (aInterfaceNames == null) {
    		return;
    	}
    	
    	for (String interfaceName : aInterfaceNames) {
    		bytecodeType.addInterfaceName(interfaceName.replace('/', '.'));
    	}
    }

    private void handleAnnotationStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationStartBytecodeEvent aAnnotationStartBytecodeEvent)
    {
        if (inField || inMethod) {
            return;
        }

        final String annotationDescriptor = aAnnotationStartBytecodeEvent.getDescriptor();
        bytecodeType.addAnnotationName(annotationDescriptor.substring(1, annotationDescriptor.length() - 1).replace('/', '.'));
    }

    private void handleClassEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassEndBytecodeEvent aClassEndBytecodeEvent)
    {
        try {
            bytecodeStream.write(classWriter.toByteArray());
        } catch (final IOException e) {
            throw new BytecodeException(e);
        }

        if (!isNatural) {
            Bytecode.getStore().registerClass(bytecodeType.getTypeName(), bytecodeStream.getBytes());
        }

        Bytecode.getStore().registerType(bytecodeType);
    }
}
